#!/usr/bin/env python

##  flamingo - 2D Game Engine
##  Copyright (C) 2009 Bradley Zeis
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##  Bradley Zeis
##  flamingoengine@gmail.com

"""Flamingo audio mixer backend for the PyAudio PortAudio bindings.

This module is heavily influenced by SWMixer by Nathan Whitehead.
"""

import time
import wave
import thread
import math

import numpy
import pyaudio

from ... import core
from ... import flmath
from ... import util
import abstractmixer

##-------- Constants
SAMPLERATE = 0
CHANNELS = 0
CHUNKSIZE = 0

PYAUDIO = None
STREAM = None

THREADED = True
THREAD = None
LOCK = thread.allocate_lock()

half_pi = math.pi * 0.5

##-------- Initialization Functions
def init(samplerate=44100, chunksize=256):
    """Initialize the mixer.

    samplerate      - (int) The number of samples per second.
    chunksize       - (int) The number of samples processed at a time.

    Returns: None
    """
    global SAMPLERATE, CHANNELS, CHUNKSIZE, PYAUDIO, STREAM
    SAMPLERATE = samplerate
    CHANNELS = 2
    CHUNKSIZE = chunksize

    PYAUDIO = pyaudio.PyAudio()
    STREAM = PYAUDIO.open(
        format = pyaudio.paInt16,
        channels = CHANNELS,
        rate = SAMPLERATE,
        output= True
    )

    Channel()

def start():
    """Start a new mixing thread."""
    global THREAD
    def f():
        while True:
            try: Mixer.tick(core.GAME.data.delta_time)
            except AttributeError: pass
    THREAD = thread.start_new_thread(f, ())

def quit():
    pass

##-------- Audio Functions
def resample(src, scale):
    """Resample a mono or stereo source."""
    n = round(len(src) * scale)
    return numpy.interp(
        numpy.linspace(0.0, 1.0, n, endpoint=False),
        numpy.linspace(0.0, 1.0, len(src), endpoint=False),
        src
    )

def interleave(left, right):
    """Convert two mono sources into one stereo source."""
    return numpy.ravel(numpy.vstack((left, right)), order='F')

def uninterleave(src):
    """Convert one stereo source into two mono sources."""
    return src.reshape(2, len(src)/2, order='FORTRAN')

def stereo_to_mono(left, right):
    """Convert one stereo source into one mono source."""
    return (0.5 * left + 0.5 * right).astype(numpy.int16)

def calculate_pan(left, right, pan):
    """Pan two mono sources in the stereo field."""
    if pan < -1: pan = -1
    elif pan > 1: pan = 1

    pan = ((pan + 1.0) * 0.5) * half_pi
    l, r = math.cos(pan), math.sin(pan)

    return left * l, right * r

def __distance__(distance):
    if distance > 1:
        v = distance**-1
        if v > 0.01:
            return v
        return 0
    return 1

distance_formula = __distance__

def ms_to_samp(ms):
    return int(ms * (SAMPLERATE / 1000.0))

def samp_to_ms(samp):
    return int(samp / (SAMPLERATE / 1000.0))

##-------- Sounds
class Sound(abstractmixer.Sound):
    """A sound completely loaded into memory."""

    def __init__(self, filename):

        ## See if data is cached
        try:
            self.data = core._Data.aud_cache[filename]
        except KeyError:

            ## Read Data
            wav = wave.open(filename, 'rb')
            num_ch = wav.getnchannels()
            self.framerate = wav.getframerate()
            fr = wav.getframerate()
            sw = wav.getsampwidth()
            data = []
            r = ' '
            while r != '':
                r = wav.readframes(4096)
                data.append(r)
            if sw == 2:
                self.data = numpy.fromstring(''.join(data), dtype=numpy.int16)
            if sw == 4:
                self.data = numpy.fromstring(''.join(data), dtype=numpy.int32)
                self.data /= 65536.0
            if sw == 1:
                self.data = numpy.fromstring(''.join(data), dtype=numpy.uint8)
                self.data *= 256.0
            wav.close()

            ## Resample
            if fr != SAMPLERATE:
                scale = SAMPLERATE * 1.0 / fr
                if num_ch == 1:
                    self.data = resample(self.data, scale)
                if num_ch == 2:
                    left, right = uninterleave(self.data)
                    nleft = resample(left, scale)
                    nright = resample(right, scale)
                    self.data = interleave(nleft, nright)

            ## Stereo Conversion
            if num_ch != CHANNELS:
                if num_ch == 1:
                    ## Need Stereo
                    self.data = interleave(self.data, self.data)

            core._Data.aud_cache[filename] = self.data

    def play(self, channel=0, volume=1.0, loops=0, pos=(0,0)):
        """Plays the audio data.

        channel     - Mixer channel to play the audio on.
        volume      - The volume to play at.
        loops       - The number of times to loop the audio. -1 is infinite
        pos         - The position in 2d space to play the sound at.

        Returns: SoundEvent
        """
        sndevent = SoundEvent(self.data, volume, loops, flmath.Vector(pos))
        if Mixer.channels == []:
            Channel()
        LOCK.acquire()
        Mixer.channels[channel].add_src(sndevent)
        LOCK.release()
        return sndevent

    def resample(self, samplerate):
        self.data = resample(self.data, SAMPLERATE * 1.0 / samplerate)

    ##-------- Properties
    def _get_length(self):
        return len(self.data)
    length = property(_get_length)

class StreamingSound(abstractmixer.StreamingSound):
    """A sound streamed directly from disk."""

    def __init__(self, filename):
        self.filename = filename

    def play(self, channel=0, volume=1.0, loops=0):
        """Plays the audio stream.

        channel     - Mixer channel to play the audio on.
        volume      - The volume to play at.
        loops       - The number of times to loop the audio. -1 is infinite

        Returns: StreamingSoundEvent
        """
        sndevent = StreamingSoundEvent(self.filename, volume, loops)
        if Mixer.channels == []:
            Channel()
        LOCK.acquire()
        Mixer.channels[channel].add_src(sndevent)
        LOCK.release()
        return sndevent

    ## Properties won't work, they're just there for compatibility
    ## with Sound
    ##-------- Properties
    def _get_length(self):
        return None
    length = property(_get_length)

class SoundEvent(object):
    """Represents one Sound source currently playing."""
    def __init__(self, data, volume, loops, pos):
        self.data = data
        self.seek = 0
        self.volume = volume
        self.loops = loops
        self.pos = pos

        self.active = True
        self.done = False

    def get_samples(self, sz):
        """Return a new buffer of samples from the audio data.

        sz      - The size, in samples, of the buffer

        Returns: numpy.array
        """
        if not self.active:
            return

        z = self.data[self.seek:self.seek + sz]
        self.seek += sz
        if len(z) < sz:
            # oops, sample data didn't cover buffer
            if self.loops != 0:
                # loop around
                self.loops -= 1
                self.seek = sz - len(z)
                z = numpy.append(z, self.data[:sz - len(z)])
            else:
                # nothing to loop, just append zeroes
                z = numpy.append(z, numpy.zeros(sz - len(z), numpy.int16))
                # and stop the sample, it's done
                self.done = True
        if self.seek == len(self.data):
            # this case loops without needing any appending
            if self.loops != 0:
                self.loops -= 1
                self.seek = 0
            else:
                self.done = True
        return uninterleave(z * self.volume)

    def pause(self):
        LOCK.acquire()
        self.active = False
        LOCK.release()

    def unpause(self):
        LOCK.acquire()
        self.active = True
        LOCK.release()

    def stop(self):
        LOCK.acquire()
        self.done = True
        LOCK.release()

class StreamingSoundEvent(object):
    """Represents one StreamingSound currently playing."""
    def __init__(self, filename, volume, loops):
        self.stream = wave.open(filename, 'rb')
        self.volume = volume
        self.seek = 0
        self.loops = loops
        self.pos = (0, 0)

        self.active = True
        self.done = False
        self.buf = ''

    def get_samples(self, sz):
        """Return a new buffer of samples from the audio data.

        sz      - The size, in samples, of the buffer

        Returns: numpy.array
        """
        szb = sz * 2
        while len(self.buf) < szb:
            s = self.read()
            if s is None or s == '': break
            self.buf += s[:]
        z = numpy.fromstring(self.buf[:szb], dtype=numpy.int16)
        if len(z) < sz:
            # In this case we ran out of stream data
            # append zeros (don't try to be sample accurate for streams)
            z = numpy.append(z, numpy.zeros(sz - len(z), numpy.int16))
            if self.loops != 0:
                self.loops -= 1
                self.seek = 0
                self.stream.rewind()
                self.buf = ''
            else:
                self.done = True
                self.stream.close()
        else:
            # remove head of buffer
            self.buf = self.buf[szb:]
        return uninterleave(z * self.volume)

    def read(self):
        """Return a buffer from the audio file."""
        return self.stream.readframes(4096)

    def pause(self):
        LOCK.acquire()
        self.active = False
        LOCK.release()

    def unpause(self):
        LOCK.acquire()
        self.active = True
        LOCK.release()

    def stop(self):
        LOCK.acquire()
        self.done = True
        self.stream.close()
        LOCK.release()

##-------- Mixer
class Channel(abstractmixer.Channel):
    """Represents a channel on a hardware mixer."""

    def __init__(self, volume=1.0, pan=0):
        self.id = len(Mixer.channels)
        Mixer.channels.append(self)
        self.srcs = []
        self.effects = []
        self.volume = volume
        self.pan = 0

    def add_src(self, src):
        """Add a SoundEvent or StreamingSoundEvent to the channel."""
        self.srcs.append(src)

    def add_effect(self, effect):
        LOCK.acquire()
        self.effects.append(effect)
        LOCK.release()

class Mixer(abstractmixer.Mixer):
    """Represents a limited hardware mixer.

    A Mixer essentially acts as a Master Bus on an analogue mixer. All of
    the other Channel's output goes through the mixer to get written to
    the output stream.
    """

    channels = []
    effects = []

    volume = 1.0

    time_slept = 0              ## ms since last tick()

    @classmethod
    def tick(self, dt):
        """Write all of the audio buffers to the output stream.

        Basic Steps:
            1. Create a buffer for each Channel
            2. For each Sound on each Channel, calculate panning/volume,
               write to Channel Buffer
            3. Write each Sound to the appropriate Channel buffer
            4. Run each of the Channel's effects on it's Channel buffer
            5. Mix together all of the Channel buffers to the Mixer buffer
            6. Run Mixer effects on the Mixer buffer
            7. Write Mixer buffer to output stream

        Returns: None
        """
        sz = CHUNKSIZE * CHANNELS
        out = numpy.zeros(sz, numpy.float)
        chout = numpy.zeros(CHUNKSIZE)

        LOCK.acquire()
        for ch in self.channels:
            rmlist = []

            ## Create Channel Buffer
            choutl, choutr = chout.copy(), chout.copy()

            for src in ch.srcs:
                if not src.active:
                    continue

                ## Read Data from Sound Buffer
                l, r = src.get_samples(sz)

                if src.done:
                    rmlist.append(src)
                    continue

                ## Calculate Sound Volume/Pan from distance
                d = flmath.distance(src.pos, (0,0))
                dist_vol = distance_formula(d)
                angle_pan = 0

                if d != 0:
                    a = math.radians(src.pos.angle) / 2.0
                    angle_pan =  (2 * a) / half_pi - 1
                    if angle_pan > 1:
                        angle_pan = 1 - (angle_pan - 1)

                l *= dist_vol
                r *= dist_vol
                l, r = calculate_pan(l, r, ch.pan - angle_pan)

                ## Write Sound to Channel buffer
                choutl += l
                choutr += r

            ## Render Channel Effects
            for effect in ch.effects:
                choutl, choutr = effect.process(choutl, choutr, CHUNKSIZE)

            ## Write Channel buffer to Mixer buffer
            s = interleave(choutl, choutr) * ch.volume
            out = out + s - (out * s) / 65536

            for src in rmlist:
                ch.srcs.remove(src)

        ## Render Mixer Effects
        l, r = uninterleave(out)

        for effect in self.effects:
            effect.process(l, r, CHUNKSIZE)

        LOCK.release()

        ## Write Mixer buffer to Output Stream
        out = interleave(l, r) * self.volume
        out = out.clip(-32767.0, 32767.0)
        outdata = (out.astype(numpy.int16)).tostring()
        #while STREAM.get_write_available() < sz: time.sleep(0.001)
        STREAM.write(outdata, CHUNKSIZE)
